package argv

import (
	"fmt"
	"errors"
	"reflect"
	"strings"
)

func ParseArgs(args ([] string), ptr reflect.Value) (string, error) {
	if ptr.Kind() != reflect.Ptr {
		panic("invalid argument: expect a pointer to write data")
	}
	if ptr.Elem().Kind() != reflect.Struct {
		panic("invalid argument: expect a struct pointer to write fields")
	}
	const key_prefix = "--"
	const key_val_sep = '='
	var positional = make([] string, 0)
	var named = make(map[string] string)
	var no_more_named = false
	var arg0 = args[0]
	for _, arg := range args[1:] {
		if arg == key_prefix {
			no_more_named = true
		} else if !(no_more_named) && strings.HasPrefix(arg, key_prefix) {
			var arg = strings.TrimPrefix(arg, key_prefix)
			var n = strings.IndexRune(arg, key_val_sep)
			var key, value = (func() (string, string) {
				if n == -1 {
					return arg, ""
				} else {
					return arg[:n], arg[(n + 1):]
				}
			})()
			named[key] = value
		} else {
			positional = append(positional, arg)
		}
	}
	var positional_hint string
	var named_hint = make(map[string] string)
	var named_desc = make(map[string] string)
	var commands ([] string)
	var default_command string
	var options = make([] string, 0)
	var obj = ptr.Elem()
	var t = obj.Type()
	for i := 0; i < t.NumField(); i += 1 {
		var field = t.Field(i)
		var kind = field.Tag.Get("arg")
		var value, err = (func() (interface{}, error) {
			switch kind {
			case "positional":
				positional_hint = field.Tag.Get("hint")
				return ([] string)(positional), nil
			case "command":
				const sep = "; "
				var key = strings.Split(field.Tag.Get("key"), sep)
				var desc = strings.Split(field.Tag.Get("desc"), sep)
				commands = key
				default_command = field.Tag.Get("default")
				for i := range key {
					if i < len(desc) {
						named_desc[key[i]] = desc[i]
					}
				}
				var command = ""
				for _, item := range key {
					var _, has_item = named[item]
					if has_item {
						if command == "" {
							command = item
						} else {
							return nil, errors.New(fmt.Sprintf(
								"ambiguous command: '%s' or '%s' ?",
								command, item,
							))
						}
					}
				}
				if command == "" {
					command = default_command
				}
				return string(command), nil
			case "flag-enable":
				var key = field.Tag.Get("key")
				options = append(options, key)
				named_desc[key] = field.Tag.Get("desc")
				var _, flag_is_set = named[key]
				var enabled = flag_is_set
				return bool(enabled), nil
			case "flag-disable":
				var key = field.Tag.Get("key")
				options = append(options, key)
				named_desc[key] = field.Tag.Get("desc")
				var _, flag_is_set = named[key]
				var enabled = !(flag_is_set)
				return bool(enabled), nil
			case "value-string":
				var key = field.Tag.Get("key")
				options = append(options, key)
				named_hint[key] = field.Tag.Get("hint")
				named_desc[key] = field.Tag.Get("desc")
				return string(named[key]), nil
			default:
				return nil, errors.New("invalid argument kind: " + kind)
			}
		})()
		if err == nil {
			obj.Field(i).Set(reflect.ValueOf(value))
		} else {
			return "", err
		}
	}
	var buf strings.Builder
	if positional_hint == "" {
		positional_hint = "[ARGUMENT]..."
	}
	var options_hint = ""
	if len(options) > 0 {
		options_hint = "[OPTION]..."
	}
	fmt.Fprintf(&buf, "Usage: %s %s %s\n", arg0, options_hint, positional_hint)
	const pad1 = "  "
	const pad2 = "    \t"
	if len(commands) > 0 {
		buf.WriteRune('\n')
		buf.WriteString("Commands:")
		buf.WriteRune('\n')
		for _, item := range commands {
			var desc = named_desc[item]
			buf.WriteString(pad1 + key_prefix + item  + pad2 + desc + "\n")
		}
	}
	if len(options) > 0 {
		buf.WriteRune('\n')
		buf.WriteString("Options:")
		buf.WriteRune('\n')
		for _, item := range options {
			var hint = named_hint[item]
			var desc = named_desc[item]
			if hint != "" {
				var key_val_sep = string([] rune { key_val_sep })
				buf.WriteString(pad1 + key_prefix + item + key_val_sep + hint)
			} else {
				buf.WriteString(pad1 + key_prefix + item)
			}
			if item == default_command {
				buf.WriteString(pad2 + "(default) " + desc)
			} else {
				buf.WriteString(pad2 + desc)
			}
			buf.WriteRune('\n')
		}
	}
	var help = buf.String()
	return help, nil
}


